package com.ehe.generator.util;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.NullCacheStorage;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.util.StringUtils;

import javax.annotation.Nonnull;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Transient;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;

class DefinitionUtil {

    ////////////////////////// 不定参 //////////////////////////////////
    /** 子工程名称 */
    private static final String PROJECT_NAME = com.ehe.generator.util.GenerateTemplateCodeUtil.PROJECT_NAME;

    /** 类名 */
    private static final String CLASS_NAME = com.ehe.generator.util.GenerateTemplateCodeUtil.CLASS_NAME;

    /** 类注释 */
    private static final String CLASS_EXPLAIN = com.ehe.generator.util.GenerateTemplateCodeUtil.CLASS_EXPLAIN;

    /** 项目根目录 */
    private static final String PROJECT_DIR = com.ehe.generator.util.GenerateTemplateCodeUtil.PROJECT_DIR;

    /** 总工程名称 */
    private static final String PARENT_PROJECT_NAME = com.ehe.generator.util.GenerateTemplateCodeUtil.PARENT_PROJECT_NAME;

    /** 子工程包名 */
    private static final String PROJECT_PACKAGE_NAME = com.ehe.generator.util.GenerateTemplateCodeUtil.PROJECT_PACKAGE_NAME;

    /** 总工程包名 */
    private static final String PARENT_PROJECT_PACKAGE_NAME = com.ehe.generator.util.GenerateTemplateCodeUtil.PARENT_PROJECT_PACKAGE_NAME;

    /** 基础包名前缀 */
    private static final String BASE_PACKAGE_PREFIX = com.ehe.generator.util.GenerateTemplateCodeUtil.BASE_PACKAGE_PREFIX;

    ////////////////////////// 定参 //////////////////////////////////
    /** 包名前缀 */
    private static final String PACKAGE_PREFIX = BASE_PACKAGE_PREFIX + PARENT_PROJECT_PACKAGE_NAME;

    /** 文件夹分隔符 */
    private static final String FILE_SEPARATOR = System.getProperty("file.separator");

    /** 基本数据类型 - 浮点数 */
    static final List<String> BASIC_CLASS_TYPE_AMOUNT = Arrays.asList("Float", "float", "Double", "double", "BigDecimal");

    /** 基本数据类型 - 整数 */
    static final List<String> BASIC_CLASS_TYPE_NUMBER = Arrays.asList("Byte", "byte", "shot", "Integer", "int", "Long", "long");

    /** 基本数据类型 - 字符 */
    static final List<String> BASIC_CLASS_TYPE_STRING = Arrays.asList("Char", "char", "String");

    /** 基本数据类型 - 布尔值 */
    static final List<String> BASIC_CLASS_TYPE_BOOLEAN = Arrays.asList("Boolean", "boolean");

    /** 基本数据类型 - 日期 */
    static final List<String> BASIC_CLASS_TYPE_LOCAL_DATE = Collections.singletonList("LocalDate");

    /** 基本数据类型 - 时间 */
    static final List<String> BASIC_CLASS_TYPE_LOCAL_DATE_TIME = Collections.singletonList("LocalDateTime");

    /** 基本数据类型 - 时间 */
    static final List<String> BASIC_CLASS_TYPE_LOCAL_TIME = Collections.singletonList("LocalTime");

    /** 基本数据类型 */
    static final List<String> BASIC_CLASS_TYPE_ALL = new ArrayList<>();

    /** 日志 */
    private static final Logger LOGGER = Logger.getLogger(DefinitionUtil.class.getName());

    static {
        BASIC_CLASS_TYPE_ALL.addAll(BASIC_CLASS_TYPE_AMOUNT);
        BASIC_CLASS_TYPE_ALL.addAll(BASIC_CLASS_TYPE_NUMBER);
        BASIC_CLASS_TYPE_ALL.addAll(BASIC_CLASS_TYPE_STRING);
        BASIC_CLASS_TYPE_ALL.addAll(BASIC_CLASS_TYPE_BOOLEAN);
        BASIC_CLASS_TYPE_ALL.addAll(BASIC_CLASS_TYPE_LOCAL_DATE);
        BASIC_CLASS_TYPE_ALL.addAll(BASIC_CLASS_TYPE_LOCAL_DATE_TIME);
    }

    /**
     * 生成Java文件
     */
    static void generateJavaFile(PackageSuffix packageSuffix) throws IOException {
        generateFile(packageSuffix, "java");
    }

    /**
     * 生成xml文件
     */
    static void generateJavaXmlFile(PackageSuffix packageSuffix) throws IOException {
        generateFile(packageSuffix, "xml");
    }

    /**
     * 生成Java文件
     */
    static void generateXmlFile(PackageSuffix packageSuffix) throws IOException {
        generateFile(packageSuffix, "xml");
    }

    /**
     * 生成Java文件
     */
    static void generateFile(PackageSuffix packageSuffix, String extensionName) throws IOException {
        String dirPath = getDir(packageSuffix);
        String fileName = packageSuffix.getClassName();
        File fileDir = new File(dirPath);
        if (!fileDir.exists()) {
            boolean success = fileDir.mkdirs();
            if (!success) {
                return;
            }
        }
        File file = new File(dirPath + fileName + "." + extensionName);
        if (!file.exists()) {
            boolean success = file.createNewFile();
            if (!success) {
                return;
            }
        } else {
            return;
        }
        Template template = getTemplate(packageSuffix.getFtl());
        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
            Writer out = new BufferedWriter(new OutputStreamWriter(fileOutputStream, "utf-8"), 10240);
            Map<String, Object> map = new HashMap<>(10);
            map.put("CONTROLLER_EXTENDS", com.ehe.generator.util.GenerateTemplateCodeUtil.CONTROLLER_EXTENDS);
            map.put("DOC_AUTHOR", com.ehe.generator.util.GenerateTemplateCodeUtil.DOC_AUTHOR);
            map.put("TABLE_NAME", com.ehe.generator.util.GenerateTemplateCodeUtil.TABLE_NAME);
            for (PackageSuffix value : PackageSuffix.values()) {
                map.put(value.name(), value);
            }
            if (packageSuffix.projectSuffix.equals(ProjectSuffix.POJO) && !packageSuffix.equals(PackageSuffix.DOMAIN)) {
                map.put("FIELD_LIST", FieldParam.getList());
            }
            template.process(map, out);
        } catch (TemplateException e) {
            e.printStackTrace();
        }
    }

    @Nonnull
    private static String getDir(@Nonnull PackageSuffix packageSuffix) {
        return PROJECT_DIR + packageSuffix.projectSuffix.getDir() + packageSuffix.env.getDir() + packageSuffix.getDir();
    }

    @Nonnull
    private static String packageConvertDir(String packageName) {
        String[] packageNames = packageName.split("\\.");
        StringBuilder packageDir = new StringBuilder();
        for (String packageNameItem : packageNames) {
            packageDir.append(packageNameItem);
            packageDir.append(FILE_SEPARATOR);
        }
        return packageDir.toString();
    }

    /***
     * 环境变量
     */
    public enum Env {
        JAVA,
        TEST,
        RESOURCES
        ;

        @Nonnull
        public String getDir() {
            String dir;
            switch (this) {
                case JAVA:
                    dir = "src" + FILE_SEPARATOR + "main" + FILE_SEPARATOR + "java" + FILE_SEPARATOR;
                    break;
                case TEST:
                    dir = "src" + FILE_SEPARATOR + "test" + FILE_SEPARATOR + "java" + FILE_SEPARATOR;
                    break;
                case RESOURCES:
                    dir = "src" + FILE_SEPARATOR + "main" + FILE_SEPARATOR + "resources" + FILE_SEPARATOR;
                    break;
                default:
                    dir = "";
            }
            return dir;
        }
    }

    public enum ProjectSuffix {
        /** 父工程名称后缀 */
        PARENT,
        /** 实体工程名称后缀 */
        POJO,
        /** 业务工程名称后缀 */
        SERVICE,
        /** 控制工程名称后缀 */
        CONTROLLER;

        public String getDir() {
            String dir = PARENT_PROJECT_NAME + "-" + PROJECT_NAME + FILE_SEPARATOR;
            if (!this.equals(PARENT)) {
                dir += PARENT_PROJECT_NAME + "-" + PROJECT_NAME + "-" + this.name().toLowerCase() + FILE_SEPARATOR;
            }
            return dir;
        }
    }

    public enum PackageSuffix {
        /** domain包名 */
        DOMAIN(Env.JAVA, ProjectSuffix.POJO),
        /** add dto包名 */
        DTO(Env.JAVA, ProjectSuffix.POJO),
        /** add dto包名 */
        ADD(Env.JAVA, ProjectSuffix.POJO),
        /** Modify dto包名 */
        MODIFY(Env.JAVA, ProjectSuffix.POJO),
        /** vo 包名 */
        VO(Env.JAVA, ProjectSuffix.POJO),
        /** 查询dto后缀 */
        SEARCHABLE(Env.JAVA, ProjectSuffix.POJO),
//        /** repository 包名 */
//        REPOSITORY(Env.JAVA, ProjectSuffix.SERVICE),
        /** mapper 包名 */
        MAPPER(Env.JAVA, ProjectSuffix.SERVICE),
        /** mapper xml */
        MAPPER_XML(Env.RESOURCES, ProjectSuffix.SERVICE),
//        /** repository 测试类 包名 */
//        REPOSITORY_TEST(Env.TEST, ProjectSuffix.SERVICE),
        /** mapper 测试类 包名 */
        MAPPER_TEST(Env.TEST, ProjectSuffix.SERVICE),
        /** service 包名 */
        SERVICE(Env.JAVA, ProjectSuffix.SERVICE),
        /** service 实现类 包名 */
        SERVICE_IMPL(Env.JAVA, ProjectSuffix.SERVICE),
        /** service 测试类 包名 */
        SERVICE_TEST(Env.TEST, ProjectSuffix.SERVICE),
        /** controller 包名 */
        CONTROLLER(Env.JAVA, ProjectSuffix.CONTROLLER),
        /** controller 实现类 包名 */
        CONTROLLER_IMPL(Env.JAVA, ProjectSuffix.CONTROLLER),
        /** controller 测试类 包名 */
        CONTROLLER_TEST(Env.TEST, ProjectSuffix.CONTROLLER),
        ;

        private static final String IMPL_SUFFIX = "_IMPL";
        private static final String TEST_SUFFIX = "_TEST";

        private Env env;

        private ProjectSuffix projectSuffix;

        PackageSuffix(Env env, ProjectSuffix projectSuffix) {
            this.env = env;
            this.projectSuffix = projectSuffix;
        }

        @Nonnull
        public String getDir() {
            return packageConvertDir(getPackage());
        }

        @Nonnull
        public String getPackage() {
            if (this.equals(MAPPER_XML)) {
                return "mapper";
            }
            String packageSuffix = this.name();
            if (this.equals(ADD) || this.equals(MODIFY) || this.equals(SEARCHABLE)) {
                packageSuffix = DTO.name();
            } else if (this.name().contains(IMPL_SUFFIX)) {
                packageSuffix = PackageSuffix.valueOf(this.name().substring(0, this.name().indexOf(IMPL_SUFFIX))).name() + ".impl";
            } else if (this.name().contains(TEST_SUFFIX)) {
                packageSuffix = PackageSuffix.valueOf(this.name().substring(0, this.name().indexOf(TEST_SUFFIX))).name();
            }
            String packageStr = PACKAGE_PREFIX + "." + packageSuffix;
            if (this.equals(DTO) || this.equals(ADD) || this.equals(MODIFY) || this.equals(SEARCHABLE)) {
                packageStr += "." + CLASS_NAME;
            }
            return packageStr.toLowerCase();
        }

        @Nonnull
        public String getClassName() {
            if (this.equals(DOMAIN)) {
                return CLASS_NAME;
            }
            String nameSuffix = upperCase(this.name().toLowerCase());
            if (this.equals(VO) || this.equals(DTO)) {
                nameSuffix = this.name();
            } else if (this.name().contains(IMPL_SUFFIX)) {
                nameSuffix = upperCase(PackageSuffix.valueOf(this.name().substring(0, this.name().indexOf(IMPL_SUFFIX))).name().toLowerCase()) + "Impl";
            } else if (this.name().contains(TEST_SUFFIX)) {
                nameSuffix = upperCase(PackageSuffix.valueOf(this.name().substring(0, this.name().indexOf(TEST_SUFFIX))).name().toLowerCase()) + "Test";
            } else if (this.name().contains("_XML")) {
                nameSuffix = upperCase(PackageSuffix.valueOf(this.name().substring(0, this.name().indexOf("_XML"))).name().toLowerCase());
            }
            return CLASS_NAME + nameSuffix;
        }

        @Nonnull
        public String getFullClassName() {
            return getPackage() + "." + getClassName();
        }

        @Nonnull
        public String getVariableName() {
            return lowerCase(getClassName());
        }

        @Nonnull
        public String getExplain() {
            String explain = " ";
            if (this.equals(DOMAIN)) {
                explain = "";
            } else if (this.equals(ADD)) {
                explain += "新增" + DTO.name();
            } else if (this.equals(MODIFY)) {
                explain += "更新" + DTO.name();
            } else if (this.equals(SEARCHABLE)) {
                explain += "查询" + DTO.name();
            } else if (this.equals(VO) || this.equals(DTO)) {
                explain += this.name();
            } else if (this.name().contains(IMPL_SUFFIX)) {
                explain += this.name().substring(0, this.name().indexOf(IMPL_SUFFIX)).toLowerCase();
            } else if (this.name().contains(TEST_SUFFIX)) {
                explain += this.name().substring(0, this.name().indexOf(TEST_SUFFIX)).toLowerCase() + " 测试";
            } else {
                explain += this.name().toLowerCase();
            }
            return CLASS_EXPLAIN + explain;
        }

        @Nonnull
        public String getFtl() {
            String name = Arrays.stream(this.name().toLowerCase().split("_"))
                    .map(DefinitionUtil::upperCase).collect(Collectors.joining());
            return lowerCase(name + ".ftl");
        }
    }

    @Getter
    @Setter
    @AllArgsConstructor
    @NoArgsConstructor
    public static class FieldParam {
        private String type;

        private String typeColumn;

        private String name;

        private String nameColumn;

        private String explain;

        private int position;

        private boolean required;

        private boolean unique;

        private boolean nullable = true;

        public FieldParam(@Nonnull Field field) {
            this.type = getTypeSimpleName(field.getGenericType());
            this.typeColumn = DefinitionUtil.getTypeColumn(field);
            this.name = field.getName();
            this.nameColumn = camelhumpAndLowercase(field.getName());
            ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
            if (apiModelProperty != null) {
                this.explain = apiModelProperty.value();
                this.position = apiModelProperty.position();
                this.required = apiModelProperty.required();
            }
            Column column = field.getAnnotation(Column.class);
            if (column != null) {
                this.unique = column.unique();
                this.nullable = column.nullable();
            }
        }

        public static List<FieldParam> getList() {
            return getField().stream().map(FieldParam::new).collect(Collectors.toList());
        }

        public static String getTypeSimpleName(Type type) {
            if (!(type instanceof ParameterizedType)) {
                return ((Class) type).getSimpleName();
            }
            ParameterizedType parameterizedType = (ParameterizedType) type;
            StringBuilder stringBuilder = new StringBuilder();
            String rawTypeName = ((Class)parameterizedType.getRawType()).getSimpleName();
            stringBuilder.append(rawTypeName);
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            if (actualTypeArguments == null || actualTypeArguments.length <= 0) {
                return stringBuilder.toString();
            }
            stringBuilder.append("<");
            boolean firstItem = true;
            for (int i = 0; i < actualTypeArguments.length; ++i) {
                Type item = actualTypeArguments[i];
                if (!firstItem) {
                    stringBuilder.append(", ");
                }
                stringBuilder.append(getTypeSimpleName(item));
                firstItem = false;
            }
            stringBuilder.append(">");
            return stringBuilder.toString();
        }
    }

    private static String getTypeColumn(@Nonnull Field field) {
        Column column = field.getAnnotation(Column.class);
        String fieldType;
        if (BASIC_CLASS_TYPE_STRING.contains(field.getType().getSimpleName())) {
            if ("createdBy".equals(field.getName()) || "modifiedBy".equals(field.getName())) {
                fieldType = "char(32)";
            } else if (column != null) {
                fieldType = "varchar(" + column.length() + ")";
            } else {
                fieldType = "varchar(255)";
            }
        } else if (BASIC_CLASS_TYPE_AMOUNT.contains(field.getType().getSimpleName())) {
            if (column != null && StringUtils.hasText(column.columnDefinition())) {
                fieldType = column.columnDefinition();
            } else {
                fieldType = "decimal(16,4)";
            }
        } else if (BASIC_CLASS_TYPE_NUMBER.contains(field.getType().getSimpleName())) {
            if (column != null && StringUtils.hasText(column.columnDefinition())) {
                fieldType = column.columnDefinition();
            } else {
                fieldType = "int";
            }
        } else if (field.getType().isEnum()) {
            if (field.getType().getGenericInterfaces()[0] instanceof ParameterizedType) {
                Type type = ((ParameterizedType) field.getType().getGenericInterfaces()[0]).getActualTypeArguments()[0];
                if (type.equals(String.class)) {
                    int length = column == null ? 50 : column.length();
                    fieldType = "varchar(" + length + ")";
                } else {
                    fieldType = "int";
                }
            } else {
                fieldType = "varchar(50)";
            }
        } else if (BASIC_CLASS_TYPE_LOCAL_DATE.contains(field.getType().getSimpleName())) {
            fieldType = "date";
        } else if (BASIC_CLASS_TYPE_LOCAL_DATE_TIME.contains(field.getType().getSimpleName())) {
            fieldType = "datetime";
        } else if (BASIC_CLASS_TYPE_LOCAL_TIME.contains(field.getType().getSimpleName())) {
            fieldType = "time";
        } else if (BASIC_CLASS_TYPE_BOOLEAN.contains(field.getType().getSimpleName())) {
            fieldType = "tinyint";
        } else {
            if (column != null) {
                fieldType = "varchar(" + column.length() + ")";
            } else {
                fieldType = "varchar(255)";
            }
        }
        return fieldType;
    }

    static List<Field> getField() {
        return getField(false);
    }

    static List<Field> getField(boolean isXml) {
        return getField(getClazz(), isXml);
    }

    private static Class getClazz() {
        Class clazz;
        try {
            clazz = Class.forName(PackageSuffix.DOMAIN.getFullClassName());
        } catch (ClassNotFoundException e) {
            LOGGER.warning(e.getMessage());
            throw new RuntimeException("class not found");
        }
        return clazz;
    }

    static String getTableName() {
        TableName table = (TableName) getClazz().getAnnotation(TableName.class);
        return table.value().toLowerCase();
    }

    private static List<Field> getField(Class clazz, boolean isXml) {
        List<Field> list = new ArrayList<>();
        if (isXml && clazz.getSuperclass() != null) {
            list.addAll(getField(clazz.getSuperclass(), true));
        }
        Field[] declaredFields = clazz.getDeclaredFields();
        for (Field field : declaredFields) {
            // default public private protected static final
            // 0 1 2 4 8 16
            if ((field.getModifiers() & 2) == 2 && (field.getModifiers() & 8) != 8 && (field.getModifiers() & 16) != 16) {
                Transient annotation = field.getAnnotation(Transient.class);
                Id annotationId = field.getAnnotation(Id.class);
                TableId annotationTableId = field.getAnnotation(TableId.class);
                if (!isXml || (annotation == null && annotationId == null && annotationTableId == null)) {
                    list.add(field);
                }
            }
        }
        return list;
    }

    public static Template getTemplate(String templateName) throws IOException {
        Configuration configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        configuration.setTemplateLoader(new ClassTemplateLoader(DefinitionUtil.class, "/templates"));
        configuration.setDefaultEncoding("UTF-8");
        configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        configuration.setCacheStorage(NullCacheStorage.INSTANCE);
        return configuration.getTemplate(templateName);
    }


    /**
     * 把一个字符串的第一个字母大写
     * @param fieldName 属性名称
     * @return 开头大写的字串
     * @author jerry.li
     */
    static String upperCase(String fieldName) {
        if (!StringUtils.hasText(fieldName) || !Character.isLowerCase(fieldName.charAt(0))) {
            return fieldName;
        }
        char[] cs = fieldName.toCharArray();
        cs[0] -= 32;
        return String.valueOf(cs);
    }

    /**
     * 把一个字符串的第一个字母小写
     * @param fieldName 属性名称
     * @return 开头小写的字串
     * @author jerry.li
     */
    static String lowerCase(String fieldName) {
        if (!StringUtils.hasText(fieldName) || !Character.isUpperCase(fieldName.charAt(0))) {
            return fieldName;
        }
        char[] cs = fieldName.toCharArray();
        cs[0] += 32;
        return String.valueOf(cs);
    }

    /**
     * 驼峰转下划线小写
     * @param name 字段名
     * @return 下划线小写
     */
    public static String camelhumpAndLowercase(String name) {
        int len = name.length();
        StringBuilder sb = new StringBuilder(len);
        for (int i = 0; i < len; i++) {
            char c = name.charAt(i);
            if (Character.isUpperCase(c) && i > 0) {
                sb.append("_");
            }
            sb.append(Character.toLowerCase(c));
        }
        return sb.toString();
    }

}
